/*
 ============================================================================
 Name        : curses-ui.c
 Author      : James McQueen, Corwyn Simpson, Craig Markham
 Version     :
 Date        : 05/14/2012
 Description : TCSS-422 Operating Systems Scheduler
 ============================================================================
 */

#include <ncurses.h>
#include <pthread.h>
#include <time.h>
#include <stdlib.h>
#include <ctype.h>

#include "IODevice.h"
#include "Process.h"
#include "PCB.h"
#include "CPU.h"
#include "logger.h"

#define KEYMAP_WIDTH 25
#define KEYMAP_MAIN 0
#define KEYMAP_NEW_PROC 1
#define KEYMAP_REQUEST_TYPE 2

#define DATA_PROC_LIST 0 
#define DATA_WATCH_PROC 1

#define INPUT_LIMIT 80
#define SECOND 1000000000 //in ns

/*----------- Model globals -----------*/
CPU* cpu;
IODevice* iodevices[NUM_IODEVICES - 1]; //kbd isn't an IODevice instance
pthread_t my_threads[NUM_IODEVICES];
pthread_attr_t plain_attr;

/*----------- UI globals -----------*/
WINDOW* keymap_win; //window for displaying the keymap on the left
int keymap_state;

WINDOW* data_win; //window for displaying information about the backend
int data_state;
PCB* data_monitor_process;

WINDOW* input_line;


/* Prints the root menu to the keymap window, and sets the state variable
 * accordingly. */
void main_keymap()
{
    wclear(keymap_win);
    keymap_state = KEYMAP_MAIN;
    wprintw(keymap_win, 
        " N - New process\n K - Kill process\n D - Process details\n");
    wprintw(keymap_win, 
        " O - Process overview\n S - Start/stop sim\n T - Set tick size\n");
    wprintw(keymap_win, " A - Generate KBD IO\n Q - Quit sim\n");
    wrefresh(keymap_win);
}

void newProc_keymap()
{
    wclear(keymap_win);
    keymap_state = KEYMAP_NEW_PROC;
    data_state = DATA_WATCH_PROC;
    wprintw(keymap_win, 
        " L - Set loop length\n A - Add Request\n D - Delete Request\n");
    wprintw(keymap_win,
        " P - Load preset\n C - Commit process\n R - Return w/o commit");
    wrefresh(keymap_win);
}

void requestType_keymap()
{
    wclear(keymap_win);
    keymap_state = KEYMAP_REQUEST_TYPE;
    wprintw(keymap_win,
        " %d - io_request\n %d - mutex_acq\n %d - mutex_give\n",
        IO_REQUEST, MUTEX_ACQ, MUTEX_GIVE);
    wprintw(keymap_win,
        " %d - shared_mem_write\n %d - shared_mem_read\n",
        SHARED_WRITE, SHARED_READ);
    wrefresh(keymap_win);
}

/* Draws the borders around the windows that we use
 */
void draw_borders()
{
    mvvline(0, 0, ACS_VLINE, LINES);
    mvvline(0, KEYMAP_WIDTH, ACS_VLINE, LINES - 3);
    mvvline(0, COLS - 1, ACS_VLINE, LINES);

    mvhline(0, 0, ACS_HLINE, COLS);
    mvhline(LINES - 3, 0, ACS_HLINE, COLS);
    mvhline(LINES - 1, 0, ACS_HLINE, COLS);

    mvaddch(0, 0, ACS_ULCORNER);
    mvaddch(0, KEYMAP_WIDTH, ACS_TTEE);
    mvaddch(0, COLS - 1, ACS_URCORNER);
  
    mvaddch(LINES - 3, 0, ACS_LTEE);
    mvaddch(LINES - 3, KEYMAP_WIDTH, ACS_BTEE);
    mvaddch(LINES - 3, COLS - 1, ACS_RTEE);
    
    mvaddch(LINES - 1, 0, ACS_LLCORNER);
    mvaddch(LINES - 1, COLS - 1, ACS_LRCORNER);
}

/* Initializes the ncurses windows and fills them with initial information.
 */
void ui_initialize()
{
    initscr(); //initialize curses screen
    halfdelay(2); //wait for 200 ms on getch()
    noecho(); //don't echo keystrokes

    //Draw the window divisions
    draw_borders();
  
    /* allocate a window nearly as tall as the viewport and just narrower
     * than a sixth of the width.*/
    keymap_win = newwin(LINES - 4, KEYMAP_WIDTH - 2, 1, 1);
    main_keymap();
    wrefresh(keymap_win);
  
    /* allocate most of the rest of the window for log output. */
    data_win = newwin(LINES - 4, COLS - KEYMAP_WIDTH - 2, 1, KEYMAP_WIDTH + 1);
    wrefresh(data_win);
  
    /* allocate the second to last line for user prompting */
    input_line = newwin(1, COLS - 2, LINES - 2, 1);
}

void printBuff(WINDOW* win, RingBuffer* buff)
{
    int i;
    for(i = buff->start; i != (buff->start + buff->size) % buff->max_size; i = (i + 1) % buff->max_size)
    {
        wprintw(win, "%d, ", pcb_getPID((PCB*) buff->elements[i]));
    }
}

/* updates the data window with the proper information based on the data_state
 */
void updateDataWin()
{
    wclear(data_win);
    //always print cpu info
    wprintw(data_win, "CPU has run for %d ticks\n", cpu_getClock(cpu));

    wprintw(data_win, "Interrupt queues:\n");
    int i;
    for (i = 0; i < NUM_IODEVICES; i++)
    {
        wprintw(data_win, "Device %d, enable bit %d: ", i, i == IODEVICE_KBD ? 1 : iodevice_isEnabled(iodevices[i]));
        printBuff(data_win, cpu->my_scheduler->my_io_blocked_queues[i]);
        wprintw(data_win, "\n");
    }
    wprintw(data_win, "\nMutex queues:\n");
    for (i = 0; i < NUM_MUTEX; i++)
    {
        wprintw(data_win, "Mutex lock %d owned by PID %d: ", i, cpu->my_scheduler->my_mutex_locks[i]);
        printBuff(data_win, cpu->my_scheduler->my_mutex_blocked_queues[i]);
        wprintw(data_win, "\n");
    }

    switch (data_state)
    {
        case DATA_PROC_LIST:
        {
            int i = 0;
            int pcb_count = cpu_getProcessCount(cpu);
            PCB** pcb_list = cpu_getPCBList(cpu);
            wprintw(data_win, "\n PID |   PC   | Length |State| %%cpu \n");
            for (; i < pcb_count; i++)
            {
                wprintw(data_win, "%4d | 0x%04x | 0x%04x |  %c  |", 
                        pcb_getPID(pcb_list[i]),
                        pcb_getPC(pcb_list[i]),
                        process_getLength(pcb_getProcess(pcb_list[i])),
                        pcb_getState(pcb_list[i]));
                if (cpu_getClock(cpu) > 0)
                { // cpu has ticked, we can calculate the %cpu time for the process
                    wprintw(data_win, " %2.2f\n",
                        (double)process_getClock(pcb_getProcess(pcb_list[i])) / 
                        cpu_getClock(cpu));
                }
                else
                { // descriptive "n/a" because it can't be calculated
                    wprintw(data_win, "  n/a \n");
                }
            }
            //TODO: add blocked queue output, maybe
            break;
        }
        case DATA_WATCH_PROC:
        {
            wprintw(data_win, "\nProcess %d has gotten %d of %d CPU time\n", 
                pcb_getPID(data_monitor_process), 
                process_getClock(pcb_getProcess(data_monitor_process)), 
                cpu_getClock(cpu) - 
                process_getStartTime(pcb_getProcess(data_monitor_process)));
            wprintw(data_win, "PC is currently at 0x%04x of 0x%04x in state %c\n",
                pcb_getPC(data_monitor_process),
                process_getLength(pcb_getProcess(data_monitor_process)),
                pcb_getState(data_monitor_process));
            int request_size = process_getNumRequests(pcb_getProcess(data_monitor_process));
            Request** request_list = process_getRequests(pcb_getProcess(data_monitor_process));
            int i = 0;
            for(; i < request_size; i++)
            { //TODO: prettify the type of request
                wprintw(data_win, "Request %d,%d at PC 0x%04x\n",
                    request_list[i]->type,
                    request_list[i]->enumeration,
                    request_list[i]->time);
            }
        }
    }
    wrefresh(data_win);
    
    //Output the contents of the ready queue with a hack
    wclear(input_line);
    wprintw(input_line, "Ready queue: ");
    printBuff(input_line, cpu->my_scheduler->my_ready_queue);
    wrefresh(input_line);
}

/* Prompts for input from the user, and stores the result into result
 */
char* getCursesString(char* message, char* result, int size)
{
    wclear(input_line);
    wprintw(input_line, message);
    wrefresh(input_line);
    int position = 0;
    int ch = ERR; // as the true condition
    while (ch != '\n')
    {
        ch = getch();
        if (ch == '\b' && position > 0) //was a backspace
        {
            position--;
            //TODO: move the cursor back one character
//            waddch(input_line, ch); //hopefully move the cursor back one
            wrefresh(input_line);
        }
        else if (isprint(ch) && position < size - 1) //is a character to add
        { //and there's room
            result[position] = ch;
            position++;
            waddch(input_line, ch); //echo it back to the user
            wrefresh(input_line);
        }
    }
    result[position] = '\0'; //terminating null
    //clear and update the input line since we're done
    wclear(input_line);
    wrefresh(input_line);
    return result;
}

void default_processes(CPU* cpu){

	//Background request that generates all three IO Device Requests
    PCB* background = pcb_init(process_init());
    process_setLength(pcb_getProcess(background), 256);
    Request back_req1;
    back_req1.type = IO_REQUEST;
    back_req1.enumeration = IODEVICE_VID;
    back_req1.time = 32;
    process_addRequest(pcb_getProcess(background), back_req1);

    Request back_req2;
    back_req2.type = IO_REQUEST;
    back_req2.enumeration = IODEVICE_DISK;
    back_req2.time = 64;
    process_addRequest(pcb_getProcess(background), back_req2);

    Request back_req3;
    back_req3.type = IO_REQUEST;
    back_req3.enumeration = IODEVICE_KBD;
    back_req3.time = 128;
    process_addRequest(pcb_getProcess(background), back_req3);
    cpu_addProcess(cpu, background);

    //Producer process that uses mutex lock and writes to shared memory
    PCB* producer = pcb_init(process_init());
    process_setLength(pcb_getProcess(producer), 64);

    Request prod_req1;
    prod_req1.type = MUTEX_ACQ;
    prod_req1.enumeration = 0;
    prod_req1.time = 8;
    process_addRequest(pcb_getProcess(producer), prod_req1);

    Request prod_req2;
    prod_req2.type = SHARED_WRITE;
    prod_req2.enumeration = 0;
    prod_req2.time = 16;
    process_addRequest(pcb_getProcess(producer), prod_req2);

    Request prod_req3;
    prod_req3.type = MUTEX_GIVE;
    prod_req3.enumeration = 0;
    prod_req3.time = 24;
    process_addRequest(pcb_getProcess(producer), prod_req3);
    cpu_addProcess(cpu, producer);

    //Consumer process that uses mutex lock and reads shared memory
    PCB* consumer = pcb_init(process_init());
    process_setLength(pcb_getProcess(consumer), 64);

    Request cons_req1;
    cons_req1.type = MUTEX_ACQ;
    cons_req1.enumeration = 0;
    cons_req1.time = 32;
    process_addRequest(pcb_getProcess(consumer), cons_req1);

    Request cons_req2;
    cons_req2.type = SHARED_READ;
    cons_req2.enumeration = 0;
    cons_req2.time = 40;
    process_addRequest(pcb_getProcess(consumer), cons_req2);

    Request cons_req3;
    cons_req3.type = MUTEX_GIVE;
    cons_req3.enumeration = 0;
    cons_req3.time = 48;
    process_addRequest(pcb_getProcess(consumer), cons_req3);
    cpu_addProcess(cpu, consumer);


}


void backend_initialize()
{
    cpu = cpu_init();

    //Add an idle process that will eat any extra cycles
    PCB* idle = pcb_init(process_init());
    process_setLength(pcb_getProcess(idle), 2);
    cpu_addProcess(cpu, idle);
    printf("cpu initialized\n");

    // create the three mandatory processes
    iodevices[IODEVICE_TIMER] = iodevice_init(cpu, IODEVICE_TIMER, 32 , 0);
    iodevices[IODEVICE_DISK] = iodevice_init(cpu, IODEVICE_DISK, 128 , 1);
    iodevices[IODEVICE_VID] = iodevice_init(cpu, IODEVICE_VID, 512 , 1);
    printf("IODevices initialized\n");

    //create disk, video, and timer IODevices, and couple with the cpu
    pthread_attr_init(&plain_attr);
  
    pthread_create(&my_threads[0], &plain_attr, cpu_run, cpu);
    pthread_create(&my_threads[1], &plain_attr, iodevice_run, iodevices[IODEVICE_TIMER]);
    pthread_create(&my_threads[2], &plain_attr, iodevice_run, iodevices[IODEVICE_DISK]);
    pthread_create(&my_threads[3], &plain_attr, iodevice_run, iodevices[IODEVICE_VID]);
    printf("Threads started\n");

    //Start the CPU paused
    //TODO: add a barrier so that cpu_run is in the while loop after this point
    //interim hack to sleep
    logger_init("log.txt");
    sleep(1);

    default_processes(cpu);
    cpu_toggleRunState(cpu);

}

int main()
{
    char input[INPUT_LIMIT];
    int is_running = 1;
    backend_initialize();
    ui_initialize();
    main_keymap();
  
    while (is_running)
    {
        wrefresh(keymap_win);
        //update the data window
        updateDataWin();

        int ch = getch(); // waits for 200 ms because of halfdelay()
        switch (keymap_state)
        {
            case KEYMAP_MAIN:
                switch (ch)
                {
                    case 'n':
                    case 'N': //New process
                        data_monitor_process = pcb_init(process_init());
                        newProc_keymap();
                        break;
                    case 'k':
                    case 'K': // K - Kill Process
                        //ask for, find, and remove a process from the CPU
                        getCursesString("PID of process to kill:", input, INPUT_LIMIT);
                        int kill_pid = atoi(input);
                        if (kill_pid > 1) // in initializing, PID 1 is idle Process
                        { // So never remove that one
                            cpu_killProcess(cpu, atoi(input));
                        }
                        break;
                    case 'd':
                    case 'D': // D - Process details
                        //Ask for a PID to monitor and change data_* to match
                        getCursesString("PID of process to examine:", input, INPUT_LIMIT);
                        data_monitor_process = cpu_getPCB(cpu, atoi(input));
                        if (data_monitor_process != NULL)
                        {
                            data_state = DATA_WATCH_PROC;
                        }
                        
                        break;
                    case 'o':
                    case 'O': // O - Process overview 
                        data_state = DATA_PROC_LIST;
                        break;
                    case 's':
                    case 'S': // S - Start/stop
                        cpu_toggleRunState(cpu);
                        int i;
                        for (i = 0; i < NUM_IODEVICES - 1; i++)
                        {
                            iodevice_toggleEnabled(iodevices[i]);
                        }
                        break;
                    case 't':
                    case 'T': // T - Set tick size
                        //Set the tick size, in ns, of the CPU
                        getCursesString("New tick cycle size, in nanoseconds:", input, INPUT_LIMIT);
                        int ns_tick_size = atoi(input);
                        cpu->my_tick_time.tv_sec = ns_tick_size / SECOND;
                        cpu->my_tick_time.tv_nsec = ns_tick_size % SECOND;
                        break;
                    case 'a':
                    case 'A': // A - Generate KBD IO
                        //A hack a solution to put an interrupt into the CPU
                        cpu_interrupt(cpu, IODEVICE_KBD);
                        break;
                    case 'q':
                    case 'Q': // Q - Quit simulator
                        is_running = 0;
                        break;
                }
                break;
            case KEYMAP_NEW_PROC:
              //switch statement for creating a new process
                switch (ch)
                {
                    case 'l':
                    case 'L': // L - Set cycle length
                        getCursesString("New cycle length:", input, INPUT_LIMIT);
                        //TODO: error checking?
                        process_setLength(pcb_getProcess(data_monitor_process), atoi(input));
                        break;
                    case 'a':
                    case 'A': // A - Add Request
                        requestType_keymap();
                        break;
                    case 'd':
                    case 'D': // D - Delete request
                        getCursesString("Index of request to delete:", input, INPUT_LIMIT);
                        process_delRequest(pcb_getProcess(data_monitor_process), atoi(input));
                        break;
                    case 'p':
                    case 'P': // P - Load preset
                        wclear(input_line);
                        wprintw(input_line, "No presets are available.");
                        break;
                    case 'c':
                    case 'C': // C - Commit process
                        cpu_addProcess(cpu, data_monitor_process);
                        main_keymap();
                        break;
                    case 'r':
                    case 'R': // R - Return w/o commit
                        process_destroy(pcb_getProcess(data_monitor_process));
                        pcb_destroy(data_monitor_process);
                        data_monitor_process = NULL;
                        data_state = DATA_PROC_LIST;
                        main_keymap();
                        break;
                }
                break;
            case KEYMAP_REQUEST_TYPE:
                if (ch != ERR)
                { //a request type is selected
                    Request request;
                    request.type = (int) ch - '0';
                    ch = ERR; //clear "ch" so that it will get a new character for IO_REQUEST
                    //instead of the old one
                    switch (request.type)
                    {
                        case IO_REQUEST:
                            // prompt for which type of IO device the request is for
                            wclear(keymap_win);
                            wprintw(keymap_win, 
                                " %d - disk\n %d - video\n %d - keyboard\n", 
                                IODEVICE_DISK, IODEVICE_VID, IODEVICE_KBD);
                            wrefresh(keymap_win);
                            while (ch == ERR)
                            { //stay here until they make a choice
                                ch = getch();
                            }
                            //TODO: error checking that the IO device is correct
                            request.enumeration = ch - '0';
                            break;
                        case MUTEX_ACQ:
                        case MUTEX_GIVE:
                            getCursesString("Mutex to acquire/give(0 - 9):", input, INPUT_LIMIT);
                            request.enumeration = atoi(input);
                            break;
                        case SHARED_WRITE:
                        case SHARED_READ:
                            getCursesString("Shared memory address to read/write (0 - 9):", input, INPUT_LIMIT);
                            request.enumeration = atoi(input);
                            break;
                        default:
                            request.type = ERR;
                            break;
                    }
                    if (request.type != ERR)
                    {
                        getCursesString("Time step to trigger on:", input, INPUT_LIMIT);
                        request.time = atoi(input);
                        process_addRequest(pcb_getProcess(data_monitor_process), request);
                    }
                    newProc_keymap();
                }
                break;
        }
    }
    
    //cleanup, I guess
    delwin(keymap_win);
    delwin(data_win);
    endwin();
    logger_exit();
    
    return 0;
}
